/*******************************************************************************
 * Copyright (c) 2004, 2006 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.core.tests.harness;

import static java.util.Comparator.reverseOrder;
import static org.eclipse.core.tests.harness.TestHarnessPlugin.PI_HARNESS;
import static org.eclipse.core.tests.harness.TestHarnessPlugin.log;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.junit.function.ThrowingRunnable;

/**
 * Home for file system-related utility methods.
 */
public class FileSystemHelper {
	/** counter for generating unique random file system locations */
	protected static int nextLocationCounter = 0;
	private static final long MASK = 0x00000000FFFFFFFFL;

	private static Boolean canCreateSymLinks = null;

	/*
	 * Return the root directory for the temp dir.
	 */
	public static IPath getTempDir() {
		String tempPath = System.getProperty("java.io.tmpdir");
		try {
			tempPath = new java.io.File(tempPath).getCanonicalPath();
		} catch (IOException e) {
			//ignore and use non-canonical path
		}
		return IPath.fromOSString(tempPath);
	}

	/**
	 * Returns a unique location on disk. It is guaranteed that no file currently
	 * exists at that location. The returned location will be unique with respect to
	 * all other locations generated by this method in the current session. If the
	 * caller creates a folder or file at this location, they are responsible for
	 * deleting it when finished.
	 */
	public static IPath getRandomLocation() {
		return FileSystemHelper.getRandomLocation(getTempDir());
	}

	/**
	 * Returns a unique location on disk.  It is guaranteed that no file currently
	 * exists at that location.  The returned location will be unique with respect
	 * to all other locations generated by this method in the current session.
	 * If the caller creates a folder or file at this location, they are responsible for
	 * deleting it when finished.
	 */
	public static IPath getRandomLocation(IPath parent) {
		IPath path = computeRandomLocation(parent);
		while (path.toFile().exists()) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// ignore
			}
			path = computeRandomLocation(parent);
		}
		return path;
	}

	public static IPath computeRandomLocation(IPath parent) {
		long segment = (((long) ++nextLocationCounter) << 32) | (System.currentTimeMillis() & MASK);
		return parent.append(Long.toString(segment));
	}

	public static void clear(java.io.File file) {
		if (!file.exists()) {
			return;
		}
		if (file.isDirectory()) {
			String[] files = file.list();
			if (files != null) {
				for (String child : files) {
					clear(new java.io.File(file, child));
				}
			}
		}
		if (!file.delete()) {
			String message = "FileSystemHelper#clear() could not delete: " + file.getPath();
			log(new Status(IStatus.WARNING, PI_HARNESS, IStatus.OK, message, null));
		}
	}

	/**
	 * Creates a symbolic link. Should only be called on platforms where symbolic
	 * links can actually be created, i.e. an "ln" command is available.
	 *
	 * @param basedir    folder in which the symbolic link should be created
	 * @param linkName   name of the symbolic link
	 * @param linkTarget target to which the symbolic link should point
	 * @param isDir      <code>true</code> if the link should point to a folder
	 * @throws IllegalStateException in case symlink creation is not supported
	 * @throws IOException           in case the creation of the symlink failed for
	 *                               other reasons
	 */
	public static void createSymLink(File basedir, String linkName, String linkTarget, boolean isDir)
			throws IOException {
		// The following code creates even a link if
		// Files.createSymbolicLink(new File(basedir, linkName).toPath(), new
		// File(basedir, linkTarget).toPath());
		// would throw java.nio.file.FileSystemException "missing rights"
		//
		Process process = startSymlinkCreation(basedir, linkName, linkTarget, isDir);
		try {
			int exitcode = process.waitFor();
			if (exitcode != 0) {
				// xxx wrong charset. from jdk17+ we could use Console.charset()
				try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
					String result = reader.readLine();
					throw new IllegalStateException("Creating symlink is unsupported: " + result);
				}
			}
		} catch (InterruptedException e) {
			throw new IOException("Creating symlink failed due to interrupted exception", e);
		}
	}

	private static Process startSymlinkCreation(File basedir, String linkName, String linkTarget, boolean isDir)
			throws IOException {
		// Deliberately use an empty environment to make the test reproducible.
		String[] environmentParameters = {};
		if (Platform.getOS().equals(Platform.OS_WIN32)) {
			return startSymlinkCreationOnWindows(basedir, linkName, linkTarget, isDir, environmentParameters);
		}
		String[] cmd = { "ln", "-s", linkTarget, linkName };
		return Runtime.getRuntime().exec(cmd, environmentParameters, basedir);
	}

	private static Process startSymlinkCreationOnWindows(File basedir, String linkName, String linkTarget,
			boolean isDir, String[] environmentParameters) throws IOException {
		// use File.getPath to avoid 'Illegal argument - ".."' for using "../"
		// instead of "..\"
		if (isDir) {
			String[] cmd = { "cmd", "/c", "mklink", "/d", new File(linkName).getPath(),
					new File(linkTarget).getPath() };
			return Runtime.getRuntime().exec(cmd, environmentParameters, basedir);
		}
		String[] cmd = { "cmd", "/c", "mklink", new File(linkName).getPath(), new File(linkTarget).getPath() };
		return Runtime.getRuntime().exec(cmd, environmentParameters, basedir);
	}

	/**
	 * Checks whether it is possible to create a symbolic link.
	 *
	 * @return <code>true</code> if symbolic links can be created by a test
	 * @throws IOException in case an error occurred when trying to create a symlink
	 *                     for testing symlink support
	 */
	public static boolean canCreateSymLinks() throws IOException {
		if (canCreateSymLinks == null) {
			if (Platform.getOS().equals(Platform.OS_WIN32)) {
				// Creation of a symbolic link on Windows requires administrator privileges,
				// so it may or may not be possible.
				IPath tempDir = getTempDir();
				String linkName = FileSystemHelper.getRandomLocation(tempDir).lastSegment();
				try {
					// Try to create a symlink.
					createSymLink(tempDir.toFile(), linkName, "testTarget", false);
					// Clean up if the link was created.
					new File(tempDir.toFile(), linkName).delete();
					canCreateSymLinks = Boolean.TRUE;
				} catch (IllegalStateException e) {
					// This exception indicates that creation of the symlink failed.
					canCreateSymLinks = Boolean.FALSE;
				}
			} else {
				canCreateSymLinks = Boolean.TRUE;
			}
		}
		return canCreateSymLinks.booleanValue();
	}

	/**
	 * Deletes the file or folder at the given path with all its contents.
	 *
	 * @param path the path of the file or folder to delete
	 * @throws IOException if traversing the file tree for deletion fails
	 */
	public static void deleteRecursively(Path path) throws IOException {
		Files.walk(path) //
				.sorted(reverseOrder()) //
				.forEach(FileSystemHelper::deleteSilently);
	}

	private static void deleteSilently(Path path) {
		try {
			Files.delete(path);
		} catch (IOException exception) {
			ILog.get().log(new Status(IStatus.WARNING, PI_HARNESS,
					"Test file or directory could not be removed: " + path, exception));
		}
	}

	/**
	 * Deletes the file or folder at the given path with all its contents when the
	 * Java runtime is shut down.
	 *
	 * @param path the path of the file or folder to delete on shutdown
	 */
	public static void deleteOnShutdownRecursively(Path path) {
		Runnable deleteDirectory = () -> {
			try {
				deleteRecursively(path);
			} catch (IOException exception) {
				ILog.get().log(new Status(IStatus.WARNING, PI_HARNESS, "Error when removing test directory: " + path,
						exception));
			}
		};
		Runtime.getRuntime().addShutdownHook(new Thread(deleteDirectory));
	}

	/**
	 * Recursively deletes the folder at the given path after executing the given
	 * runnable.
	 *
	 * @param pathToDelete       the path of the file or folder to delete
	 * @param operationToExecute the operation to execute
	 * @throws Throwable if a throwable is thrown in the operation to execute
	 */
	public static void deleteAfterExecution(Path pathToDelete, ThrowingRunnable operationToExecute) throws Throwable {
		try {
			operationToExecute.run();
		} finally {
			FileSystemHelper.deleteRecursively(pathToDelete);
		}
	}

}
